export { commonInputFS } from './common'
export { proxyEsbuild } from './esbuild/proxy'
export { ILogger } from './logger'
export { mergeRollupPlugin } from './rollup/merge'
export * from './types'
export { mergeVitePlugin } from './vite/merge'
import { attachScopes, createFilter, dataToEsm, extractAssignedNames, makeLegalIdentifier } from '@rollup/pluginutils'
import _MagicString from 'magic-string'
import path from 'path'
import './esbuild/init'
import { mergeRollupPlugin } from './rollup/merge'
import {
  CompluginFactory,
  CompluginInstance,
  CreateComplugin,
  CreateCompluginArgs,
  RegisterCompluginGenerator
} from './types'
import {
  doHash,
  escapeRegExp,
  esm_default,
  generateCodeFrame,
  generateCodeFrameMessage,
  lookupName,
  mergeSourceMap,
  normalizePath,
  slash,
  toArray,
  toBuffer,
  toDataUri,
  toIdentifier,
  _require
} from './utils'

export const MagicString = _MagicString

const generators: Record<
  string | symbol,
  <UserOptions = {}>(args: CreateCompluginArgs<UserOptions>, userOptions?: UserOptions) => any
> = Object.create(null)

export const registerCompluginGenerator: RegisterCompluginGenerator = (framework, generator) => {
  if (typeof generator !== 'function') {
    throw new TypeError(`Parameter 'generator' must be a function.`)
  }
  try {
    generators[framework] = generator
  } catch {}
}

export const createComplugin: CreateComplugin = args => {
  const getInstance = (userOptions?: any) => {
    const cache = Object.create(null)

    return new Proxy(Object.create(null), {
      get: (target, p, receiver) => {
        if (p === '$complugin') {
          return compluginProxy
        } else if (p in generators) {
          return cache[p] ?? (cache[p] = generators[p](args, userOptions))
        } else {
          return Reflect.get(target, p, receiver)
        }
      },
      has: (target, p) => {
        return p in generators || Reflect.has(target, p)
      },
      ownKeys: target => {
        return [...new Set([...Object.keys(generators), ...Array.from(Reflect.ownKeys(target))])]
      }
    })
  }

  const cache = Object.create(null)

  const compluginProxy = new Proxy(getInstance, {
    get: (target, p, receiver) => {
      if (p === '$complugin') {
        return compluginProxy
      } else if (p === 'raw') {
        return args.factory
      } else if (p in generators) {
        return cache[p] ?? (cache[p] = (userOptions: any) => generators[p](args, userOptions))
      } else {
        return Reflect.get(target, p, receiver)
      }
    },
    has: (target, p) => {
      return p === 'raw' || p in generators || Reflect.has(target, p)
    },
    ownKeys: target => {
      return [...new Set(['raw', ...Object.keys(generators), ...Array.from(Reflect.ownKeys(target))])]
    }
  }) as any

  return compluginProxy
}

const js_paths = {
  vite: path.join(__dirname, 'vite.js'),
  rollup: path.join(__dirname, 'rollup.js'),
  esbuild: path.join(__dirname, 'esbuild.js'),
  webpack: path.join(__dirname, 'webpack.js')
}

Object.defineProperties(
  generators,
  Object.fromEntries(
    Object.entries(js_paths).map(([key, js_path]) => {
      return [
        key,
        {
          configurable: true,
          enumerable: true,
          get: () => {
            const mod = esm_default(_require(js_path))

            Object.defineProperty(generators, key, {
              configurable: true,
              enumerable: true,
              value: mod
            })

            return mod
          }
        }
      ]
    })
  )
)

export const utils = {
  escapeRegExp,
  mergeSourceMap,
  toDataUri,
  toArray,
  toBuffer,
  slash,
  normalizePath,
  makeLegalIdentifier,
  createFilter,
  dataToEsm,
  attachScopes,
  extractAssignedNames,
  lookupName,
  generateCodeFrame,
  generateCodeFrameMessage,
  toIdentifier,
  hash: doHash
}

export const fromUnplugin = <UserOptions = {}>(
  unplugin: import('unplugin').UnpluginInstance<UserOptions>
): CompluginInstance<UserOptions> => {
  const unpluginFactory = unplugin.raw

  const compluginFactory: CompluginFactory<import('unplugin').UnpluginOptions> = (unplugin, meta) => {
    const { watchChange, transformInclude, transform, load, resolveId, buildStart, buildEnd } = unplugin!

    switch (meta.framework) {
      case 'vite':
        break
      case 'rollup':
        meta.plugin.watchChange = watchChange
        break
      case 'webpack':
        unplugin!.webpack?.(meta.compiler)
        break
      case 'esbuild':
        const { esbuild } = unplugin!
        if (esbuild) {
          esbuild.setup?.(meta.build)
          meta.onResolveRegExp = esbuild.onResolveFilter
          if (esbuild.onLoadFilter) {
            meta.addLoadInclude(esbuild.onLoadFilter!)
          }
        }
        break
    }

    return {
      buildStart,
      transformInclude,
      transform,
      load,
      resolveId,
      buildEnd
    } as any
  }

  const buildPlugin = (userOptions: any, framework: string) => {
    const unpluginOptions = unpluginFactory(userOptions, { framework } as any)

    const finalPlugin = generators[framework](
      {
        name: unpluginOptions.name,
        enforce: unpluginOptions.enforce,
        factory: compluginFactory
      },
      userOptions
    )

    if (framework === 'vite') {
      const { vite } = unpluginOptions
      if (vite) {
        return [{ name: finalPlugin.name, enforce: finalPlugin.enforce, ...vite }, finalPlugin]
      }
    } else if (framework === 'rollup') {
      const { rollup } = unpluginOptions
      if (rollup) {
        mergeRollupPlugin(finalPlugin, rollup)
      }
    }

    return finalPlugin
  }

  const getInstance = (userOptions?: any) => {
    const cache = Object.create(null)

    return new Proxy(Object.create(null), {
      get: (target, p, receiver) => {
        if (p === '$complugin') {
          return compluginProxy
        } else if (p in generators) {
          return cache[p] ?? (cache[p] = buildPlugin(userOptions, p as any))
        } else {
          return Reflect.get(target, p, receiver)
        }
      },
      has: (target, p) => {
        return p === '$complugin' || p in generators || Reflect.has(target, p)
      },
      ownKeys: target => {
        return [...new Set(['$complugin', ...Object.keys(generators), ...Array.from(Reflect.ownKeys(target))])]
      }
    })
  }

  const cache = Object.create(null)

  let factory: CreateCompluginArgs<any>['factory']

  const compluginProxy = new Proxy(getInstance, {
    get: (target, p, receiver) => {
      if (p === '$complugin') {
        return compluginProxy
      } else if (p === 'raw') {
        return (
          factory ??
          (factory = (userOptions, meta) => compluginFactory.call(meta, unpluginFactory(userOptions, meta), meta))
        )
      } else if (p in generators) {
        return cache[p] ?? (cache[p] = (userOptions: any) => buildPlugin(userOptions, p as any))
      } else {
        return Reflect.get(target, p, receiver)
      }
    },
    has: (target, p) => {
      return p === 'raw' || p === '$complugin' || p in generators || Reflect.has(target, p)
    },
    ownKeys: target => {
      return [...new Set(['raw', '$complugin', ...Object.keys(generators), ...Array.from(Reflect.ownKeys(target))])]
    }
  }) as any

  return compluginProxy
}
